package com.minio.tool;

import cn.hutool.core.io.IoUtil;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.minio.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.Assert;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncoderException;
import ws.schild.jave.InputFormatException;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.info.MultimediaInfo;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
public class MinioTool {

    public final String DEFAULT_MEDIA_TYPE = "application/octet-stream";

    private MinioClient minioClient;

    @Resource
    @Getter
    MinioProperties minioProperties;

    @Resource(name = "minioThreadPool" , type = ThreadPoolExecutor.class)
    ThreadPoolExecutor threadPoolExecutor;

    /**
     * 初始化minioClient
     */
    @PostConstruct
    public void init() {
        minioClient = MinioClient.builder()
                .endpoint(minioProperties.getHost())
                .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                .build();
        log.debug("MinioClient Initializing Success : {}", minioClient);
    }

    /**
     * 上传文件
     *
     * @param originFileName 用户传递的文件名
     * @param inputStream    用户传递的文件流
     * @return 上传文件路径
     */
    public FileInfo upload(String originFileName, InputStream inputStream) {
        // 得到文件后缀名
        String suffix = getFileSuffix(originFileName);
        // 得到文件对应的类型
        String contentType = getFileContentType(suffix);
        // 根据文件类型获取对应的bucket
        String bucket = getBucket(contentType);
        return upload(bucket, suffix, contentType, inputStream);
    }

    private FileInfo upload(String bucket, String suffix, String contentType, InputStream inputStream) {
        // 将文件下载到本地
        File path = download(inputStream,suffix);
        try {
            Assert.isTrue(getFileSizeInMB(path.length()) <= minioProperties.getMaxFileSize(), "单个文件不可大于"+ minioProperties.getMaxFileSize() +"MB");
        } catch (IllegalArgumentException e) {
            path.delete();
            throw e;
        }
        try {
            // 1: 校验当前桶是否存在
            BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder()
                    .bucket(bucket)  // 需要校验的桶名称
                    .build();
            boolean bucketExists = minioClient.bucketExists(bucketExistsArgs);
            // 2: 桶不存在则新建
            if (!bucketExists) {
                MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder()
                        .bucket(bucket)  // 需要新建的桶名称
                        .build();
                minioClient.makeBucket(makeBucketArgs);
            }
            // 3: 上传文件
            // 文件上传的目录
            String directory = getDirectory();
            String finalFileName = UUID.randomUUID() + suffix;
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .filename(path.getAbsolutePath())     // 指定上传的文件
                    .object(directory + finalFileName)   // 指定上传到桶中的文件名
                    .contentType(contentType)  // 指定上传文件的类型
                    .build();
            ObjectWriteResponse response = minioClient.uploadObject(uploadObjectArgs);
            String objectName = response.object(); // 文件上传成功后的路径
            // 4: 校验文件完整性
            // 创建一个临时文件
            File tempFile = File.createTempFile(UUID.randomUUID().toString(), ".temp");
            // 将上传后的文件保存到本地
            save(bucket, objectName, tempFile.getAbsolutePath());
            // 原始文件流
            InputStream originalStream = new FileInputStream(path);
            // 目标文件流
            InputStream targetStream = new FileInputStream(tempFile);
            FileInfo fileInfo = new FileInfo();
            String validateMd5 = validateFileIntegrity(originalStream, targetStream);
            if (validateMd5 != null) {
                log.debug("文件校验成功 =》 上传成功 url: {}", objectName);
                fileInfo.setUrl(objectName);
                fileInfo.setFindUrl(minioProperties.getHost() + bucket + "/" + objectName);
                fileInfo.setMd5(validateMd5);
                fileInfo.setFileName(finalFileName);
                fileInfo.setBucket(bucket);
                fileInfo.setContentType(contentType);
                // 获取文件时长 创建MultimediaObject
                MultimediaObject multimediaObject = new MultimediaObject(path);
                // 获取视频信息
                MultimediaInfo multimediaInfo = multimediaObject.getInfo();
                // 获取视频时长
                long duration = multimediaInfo.getDuration();
                fileInfo.setDuration(duration);
            } else {
                log.error("文件校验失败 =》 上传失败 url: {}", objectName);
                threadPoolExecutor.execute(() -> this.delete(bucket, objectName));  // 删除上传失败的文件
                fileInfo = null;
            }
            // 5: 删除本地文件
            threadPoolExecutor.execute(() -> {
                tempFile.delete();
                path.delete();
            });
            return fileInfo;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            log.error("文件上传异常：{}", e.getMessage());
            throw new RuntimeException(e);
        } catch (EncoderException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 分块上传文件
     * @param originFileName 用户传递的文件名
     * @param inputStream    用户传递的文件流
     * @return 上传文件路径
     */
    public FileInfo multipartUpload(String originFileName, InputStream inputStream) {
        // 得到文件后缀名
        String suffix = getFileSuffix(originFileName);
        // 得到文件对应的类型
        String contentType = getFileContentType(suffix);
        // 根据文件类型获取对应的bucket
        String bucket = getBucket(contentType);
        return multipartUpload(bucket, suffix,contentType, inputStream);
    }

    /**
     * 分块上传文件
     * @param bucket 存储桶
     * @param suffix 文件后缀
     * @param contentType 文件类型
     * @param inputStream  用户传递的文件流
     * @return 上传文件路径
     */
    public FileInfo multipartUpload(String bucket, String suffix, String contentType, InputStream inputStream) {
        // 将文件下载到本地
        File path = download(inputStream,suffix);
        // 文件上传的目录
        String directory = getDirectory();
        try {
            // 对文件进行分块
            String divideUrl = divide(path.getAbsolutePath());
            File file = new File(divideUrl);
            List<ComposeSource> composeSources = new ArrayList<>(); // 待合并的桶内原始数据
            List<String> objectNames = new ArrayList<>();   // 文件存储路径集合 用于合并完成后进行删除
            if (file.isDirectory()) {
                // 得到当前目录下的每一个文件 每一个分块文件
                File[] files = file.listFiles();
                if (files != null) {
                    // 排序 预防顺序错乱导致异常
                    Arrays.sort(files, Comparator.comparingInt(o -> Integer.parseInt(o.getName())));
                    for (int i = 0; i < files.length; i++) {
                        // 得到每一个分块文件
                        File divideFile = files[i];
                        // 上传每一个分块文件
                        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                                .bucket(bucket)
                                .filename(divideFile.getAbsolutePath())     // 指定上传的文件
                                .object(directory + "divide/" + i)   // 指定上传到桶中的文件名
                                .contentType(contentType)
                                .build();
                        ObjectWriteResponse response = minioClient.uploadObject(uploadObjectArgs);
                        String objectName = response.object();
                        objectNames.add(objectName); // 文件上传成功后的路径
                        // 构建分块数据信息
                        ComposeSource composeSource = ComposeSource.builder()
                                .bucket(bucket)
                                .object(objectName)   // 分块文件的地址
                                .build();
                        composeSources.add(composeSource);
                    }
                }
            }
            // 合并分块文件
            // 最终文件名
            String finalFileName = UUID.randomUUID().toString();
            String mergeUrl = merge(bucket, directory + finalFileName, suffix, composeSources);
            // 校验文件是否一致
            // 创建一个临时文件
            File tempFile = File.createTempFile(UUID.randomUUID().toString(), ".temp");
            // 将合并后的文件保存到本地
            save(bucket, mergeUrl, tempFile.getAbsolutePath());
            // 原始文件流
            InputStream originalStream = new FileInputStream(path);
            // 目标文件流
            InputStream targetStream = new FileInputStream(tempFile);
            FileInfo fileInfo = new FileInfo();
            String validateMd5 = validateFileIntegrity(originalStream, targetStream);
            if (validateMd5 != null) {
                log.debug("分片上传文件校验成功 =》 上传成功 url: {}", mergeUrl);
                // 删除minio中的分块文件
                threadPoolExecutor.execute(() -> delete(bucket, objectNames));
                fileInfo.setBucket(bucket);
                fileInfo.setUrl(mergeUrl);
                fileInfo.setFindUrl(minioProperties.getHost() + bucket + "/" + mergeUrl);
                fileInfo.setMd5(validateMd5);
                fileInfo.setFileName(finalFileName + suffix);
                fileInfo.setContentType(contentType);
                // 获取文件时长 创建MultimediaObject
                MultimediaObject multimediaObject = new MultimediaObject(path);
                // 获取视频信息
                MultimediaInfo multimediaInfo = multimediaObject.getInfo();
                // 获取视频时长
                long duration = multimediaInfo.getDuration();
                fileInfo.setDuration(duration);
            } else {
                log.error("分片上传文件校验失败 =》 上传失败 url: {}", mergeUrl);
                // 删除minio中的分块文件 与 合并的文件
                threadPoolExecutor.execute(() -> {
                    delete(bucket, objectNames);
                    delete(bucket, mergeUrl);
                });
                fileInfo = null;
            }
            // 删除本地分块文件
            threadPoolExecutor.execute(() -> deleteDirectoryRecursively(file));
            // 删除本地下载的文件
            threadPoolExecutor.execute(() -> {
                path.delete();
                tempFile.delete();
            });
            return fileInfo;
        } catch (IOException |ServerException | InsufficientDataException | ErrorResponseException | NoSuchAlgorithmException |
                 InvalidKeyException | InvalidResponseException | XmlParserException | InternalException e) {
            log.error("文件分片上传异常：{}", e.getMessage());
            throw new RuntimeException(e);
        } catch (EncoderException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除文件
     *
     * @param bucket     指定要删除文件的桶
     * @param objectName 指定要删除的文件全路径 不包含桶
     * @return true:删除成功 false:删除失败
     */
    public void delete(String bucket, String objectName) {
        try {
            RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
                    .bucket(bucket) // 指定要删除文件的桶
                    .object(objectName) // 指定要删除的文件全路径 不包含桶
                    .build();
            minioClient.removeObject(removeObjectArgs);
            log.warn("删除指定文件成功：bucket:{} object:{}", bucket, objectName);
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            log.error("文件删除异常：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 批量删除文件
     *
     * @param bucket      桶
     * @param objectNames 文件存储路径集合
     */
    public void delete(String bucket, List<String> objectNames) {
        List<DeleteObject> deleteObjectList = objectNames.stream().map(DeleteObject::new).toList();
        RemoveObjectsArgs removeObjectArgs = RemoveObjectsArgs.builder()
                .bucket(bucket) // 指定要删除文件的桶
                .objects(deleteObjectList) // 指定要删除的文件全路径 不包含桶
                .build();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectArgs);
        // 真正执行删除逻辑 需要得到结果 遍历一边才能删除
        results.forEach(item -> {
            try {
                DeleteError deleteError = item.get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        log.debug("批量删除文件成功 bucket:{} objectName:{}", bucket, objectNames);
    }

    /**
     * 下载文件到本地 指定路径
     *
     * @param bucket     指定要下载文件的桶
     * @param objectName 指定要下载的文件全路径 不包含桶
     * @param path       指定要下载的文件保存路径
     */
    public void save(String bucket, String objectName, String path) {
        try {
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build();
            // 得到文件的输入流
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            // 将文件下载到本地
            File url = download(inputStream, new File(path));
            // 关流
            IoUtil.close(inputStream);
            log.info("文件下载成功: path={}", url);
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            log.error("文件下载异常：{}", e.getMessage());
        }
    }

    /**
     * 校验文件完整性
     *
     * @param originFileMd5  原始文件的MD5值
     * @param targetFilePath 目标文件路径
     * @return true:校验成功 false:校验失败
     */
    public String validateFileIntegrity(String originFileMd5, String targetFilePath) throws IOException {
        InputStream targetFileInputStream = new FileInputStream(targetFilePath);
        try {
            if (originFileMd5.equals(DigestUtils.md5Hex(targetFileInputStream))) {
                return originFileMd5;
            }
            return null;
        } finally {
            IoUtil.close(targetFileInputStream);
        }
    }

    public String validateFileIntegrity(InputStream originStream, InputStream targetStream) throws IOException {
        try {
            String originMd5 = DigestUtils.md5Hex(originStream);
            String targetMd5 = DigestUtils.md5Hex(targetStream);
            if (originMd5.equals(targetMd5)) {
                return originMd5;
            }
            return null;
        } finally {
            IoUtil.close(originStream);
            IoUtil.close(targetStream);
        }
    }

    /**
     * 将文件下载到本地 返回本地文件路径
     *
     * @param inputStream 用户传递的文件流
     * @return 本地文件路径
     */
    private File download(InputStream inputStream) {
        return download(inputStream, null , ".temp");
    }

    private File download(InputStream inputStream, File file) {
        return download(inputStream, file,".temp");
    }

    private File download(InputStream inputStream, String suffix) {
        return download(inputStream, null , suffix);
    }

    private File download(InputStream inputStream, File file, String suffix) {
        try {
            if (file == null) {
                file = File.createTempFile(UUID.randomUUID().toString(), suffix);
            }
            // 将用户传递的文件写出到本地
            OutputStream outputStream = new FileOutputStream(file);
            IoUtil.copy(inputStream, outputStream);
            IoUtil.close(outputStream);
            IoUtil.close(inputStream);
            return file; // 返回文件信息
        } catch (IOException e) {
            log.error("文件下载到本地异常：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取当前日期文件夹 用于为文件保存的目录路径
     *
     * @return 文件保存的目录路径
     */
    private static String getDirectory() {
        LocalDateTime localDateTime = LocalDateTime.now();
        // 获取当前年份
        int year = localDateTime.getYear();
        // 获取当前月份
        int month = localDateTime.getMonthValue();
        // 获取当前日期
        int day = localDateTime.getDayOfMonth();
        return year + "/" + month + "/" + day + "/";
    }

    /**
     * 获取文件后缀
     *
     * @param fileName 文件名
     * @return 文件后缀
     */
    public String getFileSuffix(String fileName) {
        if (fileName == null) {
            return "";
        }
        int lastIndexOf = fileName.lastIndexOf(".");
        return fileName.substring(lastIndexOf);
    }

    /**
     * 获取文件类型
     *
     * @param fileSuffix 文件后缀
     * @return 文件类型
     */
    public String getFileContentType(String fileSuffix) {
        if (fileSuffix == null) {
            fileSuffix = "";
        }
        ContentInfo suffixName = ContentInfoUtil.findExtensionMatch(fileSuffix);
        String defaultContentType = this.DEFAULT_MEDIA_TYPE;   // 默认类型
        if (suffixName != null) {
            defaultContentType = suffixName.getMimeType();
        }
        return defaultContentType;
    }

    /**
     * 根据文件类型获取对应的bucket
     *
     * @param contentType 文件类型
     * @return bucket
     */
    public String getBucket(String contentType) {
        // 获取文件类型的前缀
        String prex = contentType.split("/")[0];
        Map<String, String> bucketMap = minioProperties.getBucket();
        boolean contained = bucketMap.containsKey(prex);
        if (contained) {
            return bucketMap.get(prex);
        } else {
            return minioProperties.getDefaultBucket();
        }
    }

    /**
     * 将给定路径的文件分成多个部分，并存储在新的目录中
     * 此方法主要用于处理大文件，通过将其分割成多个较小的部分来优化存储和处理
     *
     * @param path 文件的路径
     * @return 分区文件存储的目录路径
     * @throws IOException 如果文件读写过程中发生错误
     */
    public String divide(String path) throws IOException {
        // 创建File对象以获取文件信息
        File file = new File(path);
        // 得到当前文件路径的父级路径
        String parentPath = file.getParentFile().getAbsolutePath();
        // 分区文件存储路径，使用UUID以确保路径唯一性
        String divideFilePath = parentPath + "\\" + UUID.randomUUID() + "\\";
        // 路径不存在 则新建路径
        File divideFileParentPath = new File(divideFilePath);
        if (!divideFileParentPath.exists()) {
            divideFileParentPath.mkdirs();
        }
        // 每一块文件的大小  minio中分块文件大小必须为5MB
        int size = 1024 * 1024 * 5;

        // 文件总大小
        long fileLength = file.length();
        // 需要分配的区数量  总空间 / 每个区需要占用的空间 向上取整
        int number = (int) Math.ceil((double) fileLength / size);

        // 使用CountDownLatch来同步线程，确保所有分区操作完成后再继续执行
        CountDownLatch countDownLatch = new CountDownLatch(number);
        // 进行分区
        for (int i = 0; i < number; i++) {
            // 计算每个分片的起始位置和长度
            long start = (long) i * size;
            // 避免最后不满分片长度 直接全部获取
            long end = Math.min(start + size, fileLength);
            long length = end - start;
            final int finalI = i;
            // 使用线程池执行分区任务
            threadPoolExecutor.execute(() -> {
                // 分区文件存储实际地址
                File divideFile = new File(divideFilePath + finalI);
                // 使用 RandomAccessFile 来读取文件  方便控制每一个线程应该读取的范围
                RandomAccessFile randomAccessFile = null;
                BufferedOutputStream outputStream = null;
                try {
                    randomAccessFile = new RandomAccessFile(file, "r");
                    randomAccessFile.seek(start);
                    // 构建输出流 不断写出文件
                    outputStream = new BufferedOutputStream(new FileOutputStream(divideFile));
                    // 读取文件的缓冲区
                    byte[] buffer = new byte[1024];
                    long remaining = length;
                    while (remaining > 0) {
                        int bytesRead = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, remaining));
                        if (bytesRead == -1) {
                            break;
                        }
                        outputStream.write(buffer, 0, bytesRead);
                        remaining -= bytesRead;
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                } finally {
                    countDownLatch.countDown();
                    IoUtil.close(outputStream);
                    IoUtil.close(randomAccessFile);
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return divideFilePath;
    }

    /**
     * 指定minio桶内文件进行合并
     * minio中的分块文件进行合并处理
     *
     * @param bucket         桶名
     * @param objectName     合并后的文件存储路径
     * @param suffix         文件后缀 (如：.mp4)
     * @param composeSources 待合并的桶内原始数据
     */
    private String merge(String bucket, String objectName, String suffix, List<ComposeSource> composeSources) {
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                .bucket(bucket)
                .object(objectName + suffix)
                .sources(composeSources)
                .build();
        try {
            ObjectWriteResponse response = minioClient.composeObject(composeObjectArgs);
            log.debug("文件合并成功：{}", response.object());
            return response.object();
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            log.error("文件合并异常：{}", e.getMessage());
            return null;
        }
    }

    /**
     * 递归删除目录及其所有内容
     *
     * @param directory 要删除的目录
     * @return true: 删除成功 false: 删除失败
     */
    private boolean deleteDirectoryRecursively(File directory) {
        File[] files = directory.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    if (!deleteDirectoryRecursively(file)) {
                        return false;
                    }
                } else {
                    if (!file.delete()) {
                        log.error("删除文件失败: {}", file.getAbsolutePath());
                        return false;
                    }
                }
            }
        }
        if (!directory.delete()) {
            log.error("删除目录失败: {}", directory.getAbsolutePath());
            return false;
        }
        return true;
    }

    /**
     * 将文件字节数转换为 MB
     */
    public double getFileSizeInMB(long size) {
        return size / (1024.0 * 1024.0);
    }
}
